package bancosys.tec.services.api;

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Locale;

import jmine.tec.services.api.ServiceAssertionFailedError;
import jmine.tec.services.impl.ServicesImplMessages;
import bancosys.tec.exception.LocalizedMessageHolder;
import bancosys.tec.exception.MessageHolder;

/**
 * Classe utilitaria de asserts (baseada em junit.framework.Assert).
 * 
 * @author Rafael Volpato
 */
public final class ServiceAssert {

    /**
     * Construtor.
     */
    private ServiceAssert() {
        // vazio
    }

    /**
     * Asserts that two BigDecimals are equal concerning a delta. If they are not an ServiceAssertionFailedError is thrown with the given
     * message. If the expected value is infinity then the delta value is ignored.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     * @param tolerance tolerance
     */
    public static void assertEquals(String fieldName, BigDecimal expected, BigDecimal actual, double tolerance) {
        if (expected == null && actual == null) {
            return;
        }
        if (expected != null && actual != null) {
            if (expected.compareTo(actual) == 0) {
                return;
            }

            double diff = expected.subtract(actual).abs().doubleValue();
            if (diff <= tolerance) {
                return;
            }
        }
        if (fieldName == null) {
            fail(ServicesImplMessages.ASSERT_NUMBER_FAILED.create(format(expected), format(actual), format(BigDecimal.valueOf(tolerance))));
        } else {
            fail(ServicesImplMessages.ASSERT_NUMBER_FAILED_PREFIX.create(fieldName, format(expected), format(actual),
                    format(BigDecimal.valueOf(tolerance))));
        }
    }

    /**
     * Asserts that a condition is true. If it isn't it throws an ServiceAssertionFailedError with the given message.
     * 
     * @param fieldName message
     * @param condition condition
     */
    public static void assertTrue(String fieldName, boolean condition) {
        assertEquals(fieldName, Boolean.TRUE, Boolean.valueOf(condition));
    }

    /**
     * Asserts that a condition is true. If it isn't it throws an ServiceAssertionFailedError.
     * 
     * @param condition condition
     */
    public static void assertTrue(boolean condition) {
        assertTrue(null, condition);
    }

    /**
     * Asserts that a condition is false. If it isn't it throws an ServiceAssertionFailedError with the given message.
     * 
     * @param fieldName message
     * @param condition condition
     */
    public static void assertFalse(String fieldName, boolean condition) {
        assertEquals(fieldName, Boolean.FALSE, Boolean.valueOf(condition));
    }

    /**
     * Asserts that a condition is false. If it isn't it throws an ServiceAssertionFailedError.
     * 
     * @param condition condition
     */
    public static void assertFalse(boolean condition) {
        assertFalse(null, condition);
    }

    /**
     * Fails a test with the given message.
     * 
     * @param bundle the message bundle
     * @param key the message key
     * @param args message arguments
     * @deprecated use LocalizedMessageHolder
     */
    @Deprecated
    public static void fail(String bundle, String key, Object... args) {
        fail(new MessageHolder(bundle, key, args));
    }

    /**
     * Fails a test with the given message on bundle services-api-messages
     * 
     * @param key the message key
     * @param args message arguments
     * @deprecated use LocalizedMessageHolder
     */
    @Deprecated
    public static void fail(String key, Object... args) {
        fail(new MessageHolder("services-api-messages", key, args));
    }

    /**
     * @param holder a message holder
     */
    public static void fail(LocalizedMessageHolder holder) {
        throw new ServiceAssertionFailedError(holder);
    }

    /**
     * Fails a test with no message.
     */
    public static void fail() {
        fail(null);
    }

    /**
     * Asserts that two objects are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, Object expected, Object actual) {
        if (expected == null && actual == null) {
            return;
        }
        if (expected != null && expected.equals(actual)) {
            return;
        }
        failNotEquals(fieldName, expected, actual);
    }

    /**
     * Asserts that two objects are equal. If they are not an ServiceAssertionFailedError is thrown.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(Object expected, Object actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two Strings are equal.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, String expected, String actual) {
        if (expected == null && actual == null) {
            return;
        }
        if (expected != null && expected.equals(actual)) {
            return;
        }

        failNotEquals(fieldName, expected, actual);
    }

    /**
     * Asserts that two Strings are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String expected, String actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two doubles are equal concerning a delta. If they are not an ServiceAssertionFailedError is thrown with the given
     * message. If the expected value is infinity then the delta value is ignored.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     * @param tolerance tolerance
     */
    public static void assertEquals(String fieldName, double expected, double actual, double tolerance) {
        // handle infinity specially since subtracting to infinite values gives NaN and the
        // the following test fails
        if (Double.isInfinite(expected)) {
            if (!(expected == actual)) {
                failNotEquals(fieldName, new Double(expected), new Double(actual));
            }
        } else {
            assertEquals(fieldName, BigDecimal.valueOf(expected), BigDecimal.valueOf(actual), tolerance);
        }
    }

    /**
     * Asserts that two doubles are equal concerning a delta. If they are not an ServiceAssertionFailedError is thrown with the given
     * message. If the expected value is infinity then the delta value is ignored.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     * @param tolerance tolerance
     */
    public static void assertEquals(String fieldName, float expected, float actual, double tolerance) {
        // handle infinity specially since subtracting to infinite values gives NaN and the
        // the following test fails
        if (Float.isInfinite(expected)) {
            if (!(expected == actual)) {
                failNotEquals(fieldName, new Float(expected), new Float(actual));
            }
        } else {
            assertEquals(fieldName, new BigDecimal(Float.toString(expected)), new BigDecimal(Float.toString(actual)), tolerance);
        }
    }

    /**
     * Asserts that two doubles are equal concerning a delta. If the expected value is infinity then the delta value is ignored.
     * 
     * @param expected expected
     * @param actual actual
     * @param delta delta
     */
    public static void assertEquals(double expected, double actual, double delta) {
        assertEquals(null, expected, actual, delta);
    }

    /**
     * Asserts that two floats are equal concerning a delta. If they are not an ServiceAssertionFailedError is thrown with the given
     * message. If the expected value is infinity then the delta value is ignored.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     * @param delta delta
     */
    public static void assertEquals(String fieldName, float expected, float actual, float delta) {
        // handle infinity specially since subtracting to infinite values gives NaN and the
        // the following test fails
        if (Float.isInfinite(expected)) {
            if (!(expected == actual)) {
                failNotEquals(fieldName, new Float(expected), new Float(actual));
            }
        } else if (!(Math.abs(expected - actual) < delta)) {
            failNotEquals(fieldName, new Float(expected), new Float(actual));
        }
    }

    /**
     * Asserts that two floats are equal concerning a delta. If the expected value is infinity then the delta value is ignored.
     * 
     * @param expected expected
     * @param actual actual
     * @param delta delta
     */
    public static void assertEquals(float expected, float actual, float delta) {
        assertEquals(null, expected, actual, delta);
    }

    /**
     * Asserts that two longs are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, long expected, long actual) {
        assertEquals(fieldName, new Long(expected), new Long(actual));
    }

    /**
     * Asserts that two longs are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(long expected, long actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two booleans are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, boolean expected, boolean actual) {
        assertEquals(fieldName, new Boolean(expected), new Boolean(actual));
    }

    /**
     * Asserts that two booleans are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(boolean expected, boolean actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two bytes are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, byte expected, byte actual) {
        assertEquals(fieldName, new Byte(expected), new Byte(actual));
    }

    /**
     * Asserts that two bytes are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(byte expected, byte actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two chars are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, char expected, char actual) {
        assertEquals(fieldName, new Character(expected), new Character(actual));
    }

    /**
     * Asserts that two chars are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(char expected, char actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two shorts are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, short expected, short actual) {
        assertEquals(fieldName, new Short(expected), new Short(actual));
    }

    /**
     * Asserts that two shorts are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(short expected, short actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that two ints are equal. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(String fieldName, int expected, int actual) {
        assertEquals(fieldName, new Integer(expected), new Integer(actual));
    }

    /**
     * Asserts that two ints are equal.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertEquals(int expected, int actual) {
        assertEquals(null, expected, actual);
    }

    /**
     * Asserts that an object isn't null.
     * 
     * @param object object
     */
    public static void assertNotNull(Object object) {
        assertNotNull(null, object);
    }

    /**
     * Asserts that an object isn't null. If it is an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param object object
     */
    public static void assertNotNull(String fieldName, Object object) {
        if (object == null) {
            if (fieldName == null) {
                fail(ServicesImplMessages.ASSERT_NOT_NULL_FAILED.create(object));
            } else {
                fail(ServicesImplMessages.ASSERT_NOT_NULL_FAILED_PREFIX.create(fieldName, object));
            }
        }
    }

    /**
     * Asserts that an object is null.
     * 
     * @param object object
     */
    public static void assertNull(Object object) {
        assertNull(null, object);
    }

    /**
     * Asserts that an object is null. If it is not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param fieldName message
     * @param object object
     */
    public static void assertNull(String fieldName, Object object) {
        if (object != null) {
            if (fieldName == null) {
                fail(ServicesImplMessages.ASSERT_NULL_FAILED.create(object));
            } else {
                fail(ServicesImplMessages.ASSERT_NULL_FAILED_PREFIX.create(fieldName, object));
            }
        }
    }

    /**
     * Asserts that two objects refer to the same object. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     */
    public static void assertSame(String message, Object expected, Object actual) {
        if (expected == actual) {
            return;
        }
        failNotSame(message, expected, actual);
    }

    /**
     * Asserts that two objects refer to the same object. If they are not the same an ServiceAssertionFailedError is thrown.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertSame(Object expected, Object actual) {
        assertSame(null, expected, actual);
    }

    /**
     * Asserts that two objects refer to the same object. If they are not an ServiceAssertionFailedError is thrown with the given message.
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     */
    public static void assertNotSame(String message, Object expected, Object actual) {
        if (expected == actual) {
            failSame(message, expected, actual);
        }
    }

    /**
     * Asserts that two objects refer to the same object. If they are not the same an ServiceAssertionFailedError is thrown.
     * 
     * @param expected expected
     * @param actual actual
     */
    public static void assertNotSame(Object expected, Object actual) {
        assertNotSame(null, expected, actual);
    }

    /**
     * Assert failed because two objects does not refer to the same object.
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     */
    private static void failSame(String message, Object expected, Object actual) {
        if (message == null) {
            fail(ServicesImplMessages.ASSERT_SAME_FAILED.create(expected, actual));
        } else {
            fail(ServicesImplMessages.ASSERT_SAME_FAILED_PREFIX.create(message, expected, actual));
        }
    }

    /**
     * Falha se os dois objetos passados como parametros nao sao os mesmos
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     */
    private static void failNotSame(String message, Object expected, Object actual) {
        if (message == null) {
            fail(ServicesImplMessages.ASSERT_NOT_SAME_FAILED.create(expected, actual));
        } else {
            fail(ServicesImplMessages.ASSERT_NOT_SAME_FAILED_PREFIX.create(message, expected, actual));
        }
    }

    /**
     * Valida se dois objetos nao sao iguais.
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     */
    private static void failNotEquals(String message, Object expected, Object actual) {
        fail(format(message, expected, actual));
    }

    /**
     * Formata uma mensagem de erro
     * 
     * @param message message
     * @param expected expected
     * @param actual actual
     * @return mensagem de erro formatada
     */
    private static LocalizedMessageHolder format(String message, Object expected, Object actual) {
        if (message == null) {
            return ServicesImplMessages.ASSERT_VALUE_FAILED.create(expected, actual);
        } else {
            return ServicesImplMessages.ASSERT_VALUE_FAILED_PREFIX.create(message, expected, actual);
        }
    }

    /**
     * Formata um número em valor com locale pt.
     * 
     * @param value {@link BigDecimal}
     * @return número formatado.
     */
    private static String format(BigDecimal value) {
        if (value != null) {
            NumberFormat format = NumberFormat.getInstance(new Locale("pt"));
            int scale = value.scale() < 0 ? 0 : value.scale();
            format.setMinimumFractionDigits(scale);
            format.setMaximumFractionDigits(scale);
            return format.format(value);
        } else {
            return "";
        }
    }
}
